﻿using System;
using System.Collections.Generic;
using System.Xml.Linq;
using AzukiMap.MapSource;
using AzukiMap.Renderer;
using AzukiMap.Tools;
using AzukiMap.Tools.Logger;
using GeoAPI.CoordinateSystems.Transformations;
using GeoAPI.Geometries;
using GisSharpBlog.NetTopologySuite.Geometries;

namespace AzukiMap.Layer
{
    public class GsiLayer : TiledLayer
    {
        private List<MoveTileGsi> _moveTileMethodsGsi;
        private ICoordinateTransformation _transform4326ToMap;
        private ICoordinateTransformation _transformMapTo4326;
        private string _tileSuffix;

        public GsiLayer()
        {
            TileSourceGsi = new GsiSource();
            LayerName = TileSourceGsi.Name;
            Init();
        }

        protected override int TileWidth
        {
            get { return GsiSource.GSI_DEFAULT_TILEWIDTH; }
        }


        public GsiLayer(string name)
        {
            TileSourceGsi = new GsiSource();
            LayerName = name;
            Init();
        }

        public GsiLayer(GsiSource tileSource, string name)
            : base(tileSource, name)
        {
            TileSourceGsi = tileSource;
            Init();
        }

        public GsiSource TileSourceGsi
        {
            get { return (GsiSource) base.TileSource; }
            set { base.TileSource = value; }
        }

        private void Init()
        {
            _moveTileMethodsGsi = new List<MoveTileGsi>();
            SetTileMoveOrder();
        }

        /// <summary>
        /// Create a CoordinateTransformation to calculate tile positions.
        /// Called when this layer added on the map.
        /// </summary>
        public override void SetCoordTransformation()
        {
            base.SetCoordTransformation();

            _transformMapTo4326 = TransformUtil.GetTransformation(MapCoord.Srid, 4326);
            _transform4326ToMap = TransformUtil.GetTransformation(4326, MapCoord.Srid);
        }

        private void SetTileMoveOrder()
        {
            _moveTileMethodsGsi.Add(MoveTileUpGsi);
            _moveTileMethodsGsi.Add(MoveTileRightGsi);
            _moveTileMethodsGsi.Add(MoveTileDownGsi);
            _moveTileMethodsGsi.Add(MoveTileLeftGsi);
        }

        public override void Draw()
        {
            SetCopyrightVisibility(IsDrawable ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed);

            if (!IsVisible)
            {
                return;
            }
            if (!CheckScale())
            {
                return;
            }
            // return when not drawable
            if (!IsDrawable)
            {
                return;
            }
            double tileScale = GetTileScale(MapCoord.GeoScale);
            IPoint centerCoord = GetLonLatPoint(MapCoord.GeoCenter);
            ICoordinate tlPoint = GetTileOriginFromMapCenter(centerCoord, tileScale);
            double blLonInSec = tlPoint.X;
            double blLatInSec = tlPoint.Y;

            bool isAllTilesDrawed = false;
            bool needsMoreTile = false;
            int tilesOnSameDirection = 1;

            // Draw tiles in a spiral from center tile.
            while (!isAllTilesDrawed)
            {
                isAllTilesDrawed = true;
                foreach (MoveTileGsi moveTile in _moveTileMethodsGsi)
                {
                    for (int i = 0; i < tilesOnSameDirection; i++)
                    {
                        IPolygon tileExtent = GetTileExtent(tileScale, blLonInSec, blLatInSec);
                        // Use tiles only intersects on a map canvas.
                        if (IsOnMapCanvas(tileExtent))
                        {
                            string baseUri = GetBaseReqString(tileScale);
                            string tileReqStr = CreateTileReqString(tileScale, (int) (blLonInSec * 100),
                                                                    (int) (blLatInSec * 100));
                            if (!IsRendered(tileReqStr))
                            {
                                TileData tile = CreateTile(baseUri, tileReqStr, tileExtent, tileScale, blLatInSec);
                                Logger.Debug(
                                    "GsiLayer.Draw():"
                                    + " reqString = " + tileReqStr
                                    );
                                PlaceTileImages(_renderer.RenderTile(tile));
                                // TODO: add renderedTiles after image loaded.
                                _renderedTiles.Add(tileReqStr);
                            }
                            isAllTilesDrawed = false;
                        }
                        double[] tilePosition = moveTile(blLonInSec, blLatInSec, tileScale);
                        blLonInSec = tilePosition[0];
                        blLatInSec = tilePosition[1];
                    }
                    // Two directions use same tile number.
                    if (needsMoreTile)
                    {
                        tilesOnSameDirection++;
                        needsMoreTile = false;
                    }
                    else
                    {
                        needsMoreTile = true;
                    }
                }
            }
        }

        private const string DEFAULT_TILE_SUFFIX = "-img.png";

        public string TileSuffix
        {
            get
            {
                if ( string.IsNullOrEmpty(_tileSuffix))
                    _tileSuffix = DEFAULT_TILE_SUFFIX;

                return _tileSuffix;
            }
            set { _tileSuffix = value; }
        }

        /// <summary>
        /// 
        /// http://cyberjapandata.gsi.go.jp/data/960/new/50400000/50400000-14400000-img.png
        /// </summary>
        /// <param name="tileScaleInSec"></param>
        /// <param name="blLonInSec100"></param>
        /// <param name="blLatInSec100"></param>
        /// <returns></returns>
        protected override string CreateTileReqString(double tileScaleInSec, int blLonInSec100, int blLatInSec100)
        {
            string reqStr = string.Format("{0}/{1}-{2}{3}", blLonInSec100, blLonInSec100, blLatInSec100, TileSuffix);
            return reqStr;
        }

        private string GetBaseReqString(double tileScaleInSec)
        {
            return TileSourceGsi.BaseUris[tileScaleInSec];
        }

        protected override IPoint GetGeoPointFromTileCoord(int tx, int ty, double tileScale)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Return GSI scale that is tile width in seconds.
        /// </summary>
        /// <param name="scale"></param>
        /// <returns></returns>
        protected override double GetTileScale(double scale)
        {
            double nearestScale = 0;
            foreach (var tileScale in TileSourceGsi.TileScalesRev)
            {
                if (tileScale.Key > scale)
                {
                    break;
                }
                nearestScale = tileScale.Value;
            }

            // if there is no scale to fit layer use the last scale in the list.
            if (nearestScale == 0)
            {
                foreach (var tileScale in TileSourceGsi.TileScalesRev)
                {
                    nearestScale = tileScale.Value;
                    break;
                }
            }
            return nearestScale;
        }

        protected override ICoordinate GetTileOriginFromMapCenter(IPoint point, double tileScale)
        {
            double originLonInSec = GetTileOriginLonInSec(point.X, tileScale);
            double originLatInSec = GetTileOriginLatInSec(point.Y, tileScale);

            ICoordinate tileOrigin = new Coordinate(originLonInSec, originLatInSec);
            return tileOrigin;
        }

        private TileData CreateTile(string baseUri, string tileReqStr, IPolygon tileExtent, double tileScale,
                                    double blLatInSec)
        {
            var tileUri = new Uri(baseUri + tileReqStr, UriKind.RelativeOrAbsolute);
            int tileWidth = TileSourceGsi.TileWidth[tileScale];
            int tileHeight = GetTileHeight(GetDegreeFromSecond(blLatInSec), tileWidth);

            var tile = new TileData(tileUri, tileExtent, tileWidth, tileHeight,
                                    TileSource.Srid, TileSource.Dpi, tileScale);
            return tile;
        }

        private IPolygon GetTileExtent(double tileScale, double leftInSec, double bottomInSec)
        {
            double blLon = GetDegreeFromSecond(leftInSec);
            double blLat = GetDegreeFromSecond(bottomInSec);
            double trLon = blLon + GetDegreeFromSecond(tileScale);
            double trLat = blLat + GetDegreeFromSecond(tileScale);

            IPolygon p = GetPolygonOnMap(_geoUtil.CreateExtentPolygon(blLon, blLat, trLon, trLat));

            return p;
        }

        private double GetTileOriginLonInSec(double lon, double tileScale)
        {
            double sec = GetSecondFromDegree(lon);
            double tileXNum = Math.Floor(sec / tileScale);
            return tileScale * tileXNum;
        }

        private double GetTileOriginLatInSec(double lat, double tileScale)
        {
            double sec = GetSecondFromDegree(lat);
            double tileYNum = Math.Floor(sec / tileScale);
            return tileScale * tileYNum;
        }

        private double[] MoveTileUpGsi(double blLon, double blLat, double tileScale)
        {
            blLat = blLat + tileScale;
            return new double[2] {blLon, blLat};
        }

        private double[] MoveTileRightGsi(double blLon, double blLat, double tileScale)
        {
            blLon = blLon + tileScale;
            return new double[2] {blLon, blLat};
        }

        private double[] MoveTileDownGsi(double blLon, double blLat, double tileScale)
        {
            blLat = blLat - tileScale;
            return new double[2] {blLon, blLat};
        }

        private double[] MoveTileLeftGsi(double blLon, double blLat, double tileScale)
        {
            blLon = blLon - tileScale;
            return new double[2] {blLon, blLat};
        }

        private int GetTileHeight(double blLat, double tileWidth)
        {
            return (int) Math.Floor(tileWidth / Math.Cos(blLat * Math.PI / 180.00));
        }

        private double GetSecondFromDegree(double degree)
        {
            return degree * 60 * 60;
        }

        private double GetDegreeFromSecond(double second)
        {
            return second / 60 / 60;
        }

        /// <summary>
        /// Return a WGS84 point from a point on the map projection. 
        /// </summary>
        /// <param name="mapPoint"></param>
        /// <returns></returns>
        private IPoint GetLonLatPoint(IPoint mapPoint)
        {
            //return GeometryTransform.TransformPoint(_geoUtil, mapPoint, _transformMapTo4326.MathTransform);
            IPoint p = TransformUtil.Transform(mapPoint, _transformMapTo4326);
            p.SRID = 4326;
            return p;
        }

        /// <summary>
        /// Return a polygon as map projection from a WGS84.
        /// </summary>
        /// <param name="lonlatPoly"></param>
        /// <returns></returns>
        private IPolygon GetPolygonOnMap(IPolygon lonlatPoly)
        {
            //return GeometryTransform.TransformPolygon(_geoUtil, lonlatPoly, _transform4326ToMap.MathTransform);
            return TransformUtil.Transform(lonlatPoly, _transform4326ToMap);
        }

        public override XElement ToXml()
        {
            return base.ToXml("GsiLayer");
        }

        #region Nested type: MoveTileGsi

        private delegate double[] MoveTileGsi(double blLonInSec, double blLatInSec, double tileScale);

        #endregion
    }
}